V8 是一个由 Google 开发的开源 JavaScript 引擎(也称 JavaScript 虚拟机),目前用在 Chrome 浏览器和 Node.js 中,其核心功能是执行 JavaScript 代码
![[Pasted image 20240829144328.png]]
目前市面上有很多种 JavaScript 引擎,诸如 SpiderMonkey、V8、JavaScriptCore 等。而 V8 是当下使用最广泛的 JavaScript 引擎,我们写的 JavaScript 应用,大都跑在 V8 上
V8 之所以如此受欢迎,和它许多革命性的设计是分不开的:
可以说,V8 的出现,将 JavaScript 虚拟机技术推向了一个全新的高度
V8 是一个虚拟机,具有自己的一套指令系统,它屏蔽了不同体系结构计算机(如:不同的 CPU 指令集)、不同操作系统的差异
V8 并没有采用某种单一的解释执行或编译执行,而是混合这两种手段,我们把这种混合使用编译器和解释器的技术称为 JIT(Just In Time)技术
这是一种权衡策略,因为这两种方法都各自有自的优缺点:解释执行的启动速度快,但是执行时的速度慢;而编译执行的启动速度慢,但是执行时的速度快
![[Pasted image 20240829161403.png]]
"JIT不是简单的即时编译,而是在运行时持续优化的动态过程。它让 JavaScript 从解释型语言的性能泥潭中腾飞,成为现代高性能应用的基石。" - V8 引擎首席工程师
在 JS 的性能进化史上,JIT(Just-In-Time) 编译技术无疑是革命性的突破。作为 Google Chrome 浏览器和 Node.js 的强大心脏,V8 引擎通过 JIT 技术彻底改变了 JS 的执行方式,将一门解释型语言提升到接近原生应用的执行速度
要理解 JIT 的价值,首先需要了解传统语言的执行方式差异:
JS 作为解释型语言曾因性能问题饱受诟病,直到 V8 引擎引入 JIT 技术,完美结合了两者优点
JIT(Just-In-Time) 即时编译的核心思想是:在程序运行时将热点代码(频繁执行的代码)编译为机器码,后续执行直接运行编译后的高效机器码
作为 V8 的第一个执行层,Ignition 负责:
// JavaScript 代码示例
function sum(arr) {
let total = 0;
for (let i = 0; i < arr.length; i++) {
total += arr[i];
}
return total;
}
// 简化版字节码示例 (实际更复杂)
LdaZero // 加载 0 到累加器
Star total // 存储到变量 total
LdaSmi [0] // 加载 0
Star i // 存储到变量 i
Loop:
// ... 循环体字节码
当 Ignition 识别出热点函数后,TurboFan 接手进行深度优化:
graph LR A(字节码) --> B(中间表示 IR) B --> C(机器无关优化) C --> D(机器相关优化) D --> E(生成机器码)
TurboFan 的核心优化技术包括:
类型特化示例:
// 原始函数
function add(a, b) {
return a + b;
}
// 当观察到 a 和 b 总是数字时,生成特化版本
function optimized_add(a, b) {
// 直接使用 CPU 的数字加法指令
// 跳过类型检查
return a + b;
}
函数内联示例:
// 原始代码
function calculate(a, b) {
return add(a, b) * 2;
}
// 内联优化后等效代码
function optimized_calculate(a, b) {
// 直接展开 add 函数体
return (a + b) * 2;
}
当 TurboFan 的假设被违反时(如传入不同类型参数),执行会回退到解释器:
// 原本优化的数字相加
add(2, 3); // 优化版本执行
// 传入字符串导致优化失效
add("Hello", "World"); // 触发去优化,回退到解释器执行
JS 作为动态语言,属性访问传统上很慢
function Person(name, age) {
this.name = name;
this.age = age;
}
const p1 = new Person("Alice", 30);
const p2 = new Person("Bob", 25);
// 访问属性优化过程:
// 1. V8 创建隐藏类 C0
p1.name; // 查找开销大
// 2. 添加 name 属性,创建隐藏类 C1(继承 C0)
// 3. 添加 age 属性,创建隐藏类 C2(继承 C1)
// 内联缓存记住属性位置
// 后续访问直接使用偏移量
function createPoint(x, y) {
return { x, y };
}
function calculate() {
const points = [];
for (let i = 0; i < 1000; i++) {
// 对象不逃逸出函数作用域
points.push(createPoint(i, i*2));
}
return points;
}
// TurboFan 优化后:
// 直接在栈上分配对象,避免堆分配开销
// 避免:多态函数
function process(value) {
// 不同类型的 value 会使优化失效
return value.x + value.y;
}
// 推荐:保持参数类型一致
function processNumbers(point) {
return point.x + point.y;
}
// 创建对象时保持相同顺序
// 确保共享隐藏类
// 好:属性顺序一致
const obj1 = { a: 1, b: 2 };
const obj2 = { a: 3, b: 4 };
// 差:属性顺序不同
const obj3 = { a: 1, b: 2 };
const obj4 = { b: 4, a: 3 }; // 不同的隐藏类
适中的函数大小:
理想的热点函数:
JIT 需要在运行时编译,可能影响启动性能:
V8 解决方案:
存储字节码和编译后的机器码会增加内存:
V8 解决方案:
现代 JS 语言的特性增加编译复杂性:
V8 解决方案:
允许 JS 执行与编译同时进行,减少卡顿:
sequenceDiagram participant M as Main Thread participant B as Background Thread M ->> B: 提交编译任务 M ->> M: 继续执行字节码 B ->> B: 编译热函数 B ->> M: 完成编译 M ->> M: 切换到优化代码
使用机器学习预测哪些函数最值得优化
训练数据:
预测模型:
通过 WebAssembly 利用更多编译优化可能性:
// 将关键函数编译为 WebAssembly
const wasmCode = new Uint8Array([...]);
const module = new WebAssembly.Module(wasmCode);
const instance = new WebAssembly.Instance(module);
// 调用高性能版本
const result = instance.exports.optimizedFunction();